Sync interview config with backend account - #64
Open
alpha5611331 wants to merge 18 commits into
Open
Conversation
Rename interviewConf.username/jobDescription -> fullName/context to match the backend's account fields, with a migration so existing installs keep their data on disk under the old keys. Add account.service.ts + IPC wiring to pull the account's interview config on login/remembered session and push edits from the configuration dialog to the new backend endpoint, replacing the purely-local electron-store save. Also aligns the dialog's input cap with the backend's real 128k limit (was hardcoded to 60k). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
interviewConf lived in the electron-store-backed RuntimeConfig, but the backend is now the durable store for it. Move it into the transient AppState (same pattern as credits/userRole/providedLLMModel) so it's only ever fetched from or pushed to the backend, never written to the local config file. Existing installs get any leftover local copy dropped on next launch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…config Two problems in the sync work: The dialog keyed its init effect on appState.interviewConfig. Main broadcasts the whole app state on every updateState, and the backend ping loop does that every 5s (1s while the backend is down), so the renderer got a fresh object each time and the effect re-ran mid-edit, resetting the form. Typing or pasting a CV was impossible. Key the effect on the values instead. The store cleanup deleted the legacy on-disk interviewConf without ever pushing it to the account, so every existing user lost their CV and job description on upgrade. Read it instead of deleting, migrate it on the first pull that finds an empty account config, and only drop it once the backend confirms the write - a failed migration retries on the next launch. Also guard against the reverse data loss: a save fully replaces the stored config, so a failed startup pull would let the user overwrite the account with blanks. Track whether the config was loaded, retry the pull when the dialog opens, and block Save until it succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
legacyInterviewConf was a module-level const, read on every pull whose account had no interview_config, and clearLegacyInterviewConf only deleted the disk copy. Signing in as a second account in the same process - a new signup, or another user on a shared machine - pushed the first user's name, CV and context onto that account and showed it back to them. Clear the in-memory copy alongside the disk one, and bind the copy to the first account offered it. The claim is needed on its own: a failed push deliberately keeps the copy for retry, so clearing on success alone would still leak after an offline first launch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HealthCheckService.start() awaited pullFromBackend() before startBackendLoop/startClientLoop, and ApiClient set no request timeout anywhere, so a single stalled socket left backend liveness and 401 session-expiry detection permanently unstarted. Start both loops first and leave the pull unawaited. Also give request() an opt-in timeoutMs backed by AbortSignal.timeout, which UsersApi sets to 30s; existing callers keep their current behaviour and requestStream is left alone since suggestion streams are long-lived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An unloaded interview config reads as empty, so a failed pull toasted "Full name is not set" and sent the user into a dialog that could not save either. Check interviewConfigLoaded first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The whole app state is broadcast to the renderer on every change and the
health-check loops fire every 1-5s, so moving interviewConf into AppState
put a full CV and job description (up to 128k chars each) on the wire
several times a second. Send a {fullName, hasProfileData} summary
instead, skip the broadcast entirely when nothing changed, and have the
configuration dialog fetch the real values over a new account:get.
Measured: 790 bytes per broadcast instead of ~350 KB, and none at all
while idle.
Loading on open rather than tracking app state also means a late pull can
no longer reset the form mid-edit, and the refresh it does first stops a
save from silently discarding what another device changed.
Alongside that, four gaps found reviewing the sync work:
- Start reported "try again" without retrying; nothing else re-pulls
after a failed startup fetch, so the toast repeated forever.
- The migration claim lived in memory, so a failed migration plus a
restart let the next account inherit the first user's CV. Persist it.
- updateConfig echoed what was sent, not what the backend stored, which
it truncates.
- config:get handed the not-yet-migrated CV to the renderer. Strip it,
and merge updateConfig onto the raw stored object so the disk copy
still survives until migrated.
SPEC's privacy model still claimed this data stays on the device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both regress without any visible symptom: dropping a pre-sync CV before it has been migrated loses the only copy, and putting one back into the app-state broadcast costs hundreds of KB every few seconds. Plain Node with a module hook stubbing electron - no framework, since this is two invariants rather than a suite. Run with `pnpm test:main`. Needs Node >= 22.15 for module.registerHooks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… silent data-loss paths
pullFromBackend checked the generation before applying its read, but the failed-migration branch wrote app state directly. Both the startup pull and the dialog's own pull can reach it on first launch after an upgrade, so a failure could put Save back behind a lock the successful migration had just released. Extract the check into applyIfCurrent and route both reads through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4s was chosen to stay under the 5s loop interval, but the loops await each ping before sleeping, so the timeout can never overlap a tick. ping-client authenticates against the database, so the tight bound risked reporting a slow-but-alive backend as down on a high-latency connection, which also skips the startup config pull and blocks Start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd accuracy on privacy and configuration details
HealthCheckService.start() already leaves pullFromBackend() unawaited so a stalled socket cannot wedge the liveness and 401 loops, but login still awaited it. UsersApi allows 30s, so a hung backend held the login button on a spinner that long. Nothing needs the config before the main screen appears: Start gates on interviewConfigLoaded and retries the pull itself, and the dialog refreshes on open. Also align the full-name input cap with the backend's 1,000 (was 100), and record why an unclaimed pre-sync copy is discarded rather than kept - it is a deliberate trade against the cross-account leak, not an oversight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DEFAULT_CREDIT_PLANS quoted 600/$20, 6000/$100 and 60000/$500 against the backend real 600/$5, 3000/$20 and 30000/$150. They rendered whenever /api/payment/plans failed, so a user saw quadrupled prices and doubled credit counts, then got charged the real amount. Surface the failure instead. The renderer already handles it: usePayment throws on success:false and BuyCreditsTab shows the error banner with an empty plan grid. popular now comes from the backend payload; the renderer owns its own plan copy in planDescriptions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TS 7.0 removes baseUrl. paths has resolved relative to the containing tsconfig since 4.1, so "baseUrl": "." carried no meaning the aliases depended on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The alias resolved to ./renderer, which does not exist. vite.config.ts and tsconfig.app.json both use ./src/renderer. Dead in the root itself under "files": [], but tsconfig.electron.json extends it and paths is inherited, so the main-process build carried the broken target. No src/main file imports @/ yet, so nothing was failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Syncs the interview configuration (full name, profile/CV, context) with the user's backend account so it follows them across devices.
Backend PR: PowerInterviewAI/backend#34
interviewConf.username/jobDescription->fullName/contextto match the backend's account fieldsaccount.service.ts+ IPC wiring to pull the account's config on login and on a remembered session, and to push edits from the configuration dialogData-safety and leakage fixes
HealthCheckService.start()no longer awaits the account pull before starting the liveness and 401 loops, andApiClient.request()takes an opt-in timeout (30s forUsersApi) so a stalled socket cannot leave them permanently unstarted.Keeping the CV off the wire
The whole app state is broadcast to the renderer on every change, and the health-check loops fire every 1-5s. Holding the config in
AppStatetherefore put a full CV and job description (up to 128k chars each) on the wire several times a second.{ fullName, hasProfileData }summary; the dialog fetches the real values on demand over a newaccount:getupdateStateskips the broadcast entirely when nothing actually changedconfig:getno longer hands the not-yet-migrated copy to the renderer, whileupdateConfigstill preserves it on disk until it has been migratedMeasured: 790 bytes per broadcast instead of ~350 KB, and none at all while idle.
Loading on open rather than tracking app state also fixes two things: a late-arriving pull can no longer reset the form mid-edit, and the refresh it performs first stops a save from silently discarding a change made on another device.
Deploy order
The backend must be deployed first. Against an older backend
/api/users/me404s, the config never loads, and starting an interview is blocked for everyone who auto-updates.Test plan
tsc -p tsconfig.electron.json --noEmitandtsc -b tsconfig.jsoncleaneslint .cleanvite buildandpnpm electron:build-mainsucceedpnpm test:main- 26 checks covering the two invariants that fail silently: dropping a pre-sync CV before it has been migrated, and putting one back into the app-state broadcastKnown limitation
PATCH /me/interview-configreplaces the whole config with no version or etag. Refreshing when the dialog opens narrows the window considerably, but two devices editing concurrently is still last-write-wins.